# 第9章:服务网关Zuul 动态路由与权限过滤器

作者:小傅哥
博客:https://bugstack.cn (opens new window)

沉淀、分享、成长,让自己和他人都能有所收获!😄

# 前言介绍

在实际的业务开发中不只是将路由配置放到文件中,而是需要进行动态管理并且可以在变化时不用重启系统就可以更新。与此同时还需要在接口访问的时候,可以增加一些权限验证以防止恶意访问。

  1. Filter过滤器,通过继承实现对应方法可以进行控制过滤;
  • PRE:这种过滤器在请求被路由之前调用。我们可利用这种过滤器实现身份验证、在集群中选择请求的微服务、记录调试信息等。
  • ROUTING:这种过滤器将请求路由到微服务。这种过滤器用于构建发送给微服务的请求,并使用 Apache HttpClient 或 Netfilx Ribbon 请求微服务。
  • POST:这种过滤器在路由到微服务以后执行。这种过滤器可用来为响应添加标准的 HTTP Header、收集统计信息和指标、将响应从微服务发送给客户端等。
  • ERROR:在其他阶段发生错误时执行该过滤器。 除了默认的过滤器类型,Zuul 还允许我们创建自定义的过滤器类型。例如,我们可以定制一种 STATIC 类型的过滤器,直接在 Zuul 中生成响应,而不将请求转发到后端的微服务。
  1. 自定义路由,同构实现SimpleRouteLocator和RefreshableRouteLocator自动刷新
  • protected Map<String, ZuulRoute> locateRoutes():此方法是加载路由配置的,父类中是获取properties中的路由配置,可以通过扩展此方法,达到动态获取配置的目的
  • public Route getMatchingRoute(String path):此方法是根据访问路径,获取匹配的路由配置,父类中已经匹配到路由,可以通过路由id查找自定义配置的路由规则,以达到根据自定义规则动态分流的效果

# 环境准备

  • jdk 1.8、idea2018、Maven3
  • Spring Boot 2.0.6.RELEASE
  • Spring Cloud Finchley.SR2

# 代码示例

itstack-demo-springcloud-08
├── itstack-demo-springcloud-eureka-client
│   └── src
│       └── main
│           ├── java
│           │   └── org.itstack.demo
│           │        ├── web
│           │        │   └── EurekaClientController.java
│           │        └── EurekaClientApplication.java
│           └── resources   
│               └── application.yml
├── itstack-demo-springcloud-eureka-server
│   └── src
│       └── main
│           ├── java
│           │   └── org.itstack.demo
│           │        └── EurekaServerApplication.java
│           └── resources   
│               └── application.yml
├── itstack-demo-springcloud-hystrix-feign
│   └── src
│       └── main
│           ├── java
│           │   └── org.itstack.demo
│           │        ├── service
│           │        │   ├── hystrix
│           │        │   │   └── FeignServiceHystrix.java
│           │        │   └── FeignService.java
│           │        ├── web
│           │        │   └── FeignController.java
│           │        └── FeignApplication.java
│           └── resources   
│               └── application.yml
├── itstack-demo-springcloud-hystrix-ribbon
│   └── src
│       └── main
│           ├── java
│           │   └── org.itstack.demo
│           │        ├── service
│           │        │   └── RibbonService.java
│           │        ├── web
│           │        │   └── RibbonController.java      
│           │        └── RibbonApplication.java
│           └── resources   
│               └── application.yml
└── itstack-demo-springcloud-zuul
    └── src
        └── main
            ├── java
            │   └── org.itstack.demo   
            │        ├── config
            │        │   └── ZuulConfig.java
            │        ├── filter
            │        │   └── TokenFilter.java
            │        ├── router
            │        │   └── RouteLocator.java
            │        ├── service
            │        │   └── RefreshRouteService.java
            │        └── ZuulApplication.java
            └── resources   
                └── application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

完整代码欢迎关注公众号:bugstack虫洞栈 回复“SpringCloud专题”进行下载

itstack-demo-springcloud-zuul & 动态路由与权限过滤

  1. 通过RouteLocator实现自己的动态路由配置,其实就是把配置文件内容转移到这里用代码类实现,并且可以根据需要修改为从数据库里获取。

  2. TokenFilter提供了权限验证功能,当用户访问时候会带上token否则拦截

  3. 此外还提供了自动刷新的接口,用于外部调用刷新配置

  4. 最后我们需要修改application配置,zuul中还需要排除不做路由的接口[刷新权限接口]

config/ZuulConfig.java & 路由配置类

/**
 * 路由配置
 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例
 * 论坛:http://bugstack.cn
 * Create by 付政委 on @2019
 */
@Configuration
public class ZuulConfig {

    @Autowired
    private ZuulProperties zuulProperties;
    @Autowired
    private ServerProperties server;

    @Bean
    public RouteLocator routeLocator() {
        return new RouteLocator(this.server.getServlet().getPath(), this.zuulProperties);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

filter/TokenFilter.java & 权限校验类

/**
 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例
 * 论坛:http://bugstack.cn
 * Create by 付政委 on @2019
 */
public class TokenFilter extends ZuulFilter {

    /**
     * 过滤器的类型,它决定过滤器在请求的哪个生命周期中执行。
     * FilterConstants.PRE_TYPE:代表会在请求被路由之前执行。
     * PRE、ROUTING、POST、ERROR
     */
    public String filterType() {
        return FilterConstants.PRE_TYPE;
    }

    /**
     * filter执行顺序,通过数字指定。[数字越大,优先级越低]
     */
    public int filterOrder() {
        return 0;
    }

    /**
     * 判断该过滤器是否需要被执行。这里我们直接返回了true,因此该过滤器对所有请求都会生效。
     * 实际运用中我们可以利用该函数来指定过滤器的有效范围。
     */
    public boolean shouldFilter() {
        return true;
    }

    /*
     * 具体执行逻辑
     */
    public Object run() {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        String token = request.getParameter("token");
        if (token == null || token.isEmpty()) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(401);
            ctx.setResponseBody("refuse! token is empty");
        }
        return null;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47

router/RouteLocator.java & 路由类

/**
 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例
 * 论坛:http://bugstack.cn
 * Create by 付政委 on @2019
 */
public class RouteLocator extends SimpleRouteLocator implements RefreshableRouteLocator {

    private ZuulProperties properties;

    public RouteLocator(String servletPath, ZuulProperties properties) {
        super(servletPath, properties);
        this.properties = properties;
    }

    @Override
    public void refresh() {
        doRefresh();
    }

    @Override
    protected Map<String, ZuulRoute> locateRoutes() {
        LinkedHashMap<String, ZuulRoute> routesMap = new LinkedHashMap<String, ZuulRoute>();
        //从application.properties中加载路由信息
        routesMap.putAll(super.locateRoutes());
        //从db中加载路由信息
        routesMap.putAll(routesConfigGroup());
        //优化一下配置
        LinkedHashMap<String, ZuulRoute> values = new LinkedHashMap<>();
        for (Map.Entry<String, ZuulRoute> entry : routesMap.entrySet()) {
            String path = entry.getKey();
            // Prepend with slash if not already present.
            if (!path.startsWith("/")) {
                path = "/" + path;
            }
            if (StringUtils.hasText(this.properties.getPrefix())) {
                path = this.properties.getPrefix() + path;
                if (!path.startsWith("/")) {
                    path = "/" + path;
                }
            }
            values.put(path, entry.getValue());
        }
        return values;
    }

    /**
     * 路由配置组,可以从数据库中读取
     * 基本配置与写在文件中配置类似,如下;
     * #  routes:
     * #    api-a:
     * #      path: /route-a/**
     * #      serviceId: itstack-demo-springcloud-feign
     * #    api-b:
     * #      path: /route-b/**
     * #      serviceId: itstack-demo-springcloud-ribbon
     * @return 配置组内容
     */
    private Map<String, ZuulRoute> routesConfigGroup() {
        Map<String, ZuulRoute> routes = new LinkedHashMap<>();

        ZuulRoute zuulRoute = new ZuulRoute();
        zuulRoute.setId("route-a");
        zuulRoute.setPath("/route-a/**");
        zuulRoute.setServiceId("itstack-demo-springcloud-feign");
        // 如果使用了注册中心,那么可以根据serviceId进行访问。
        // zuulRoute.setUrl("http://localhost:9001");
        zuulRoute.setRetryable(false);
        zuulRoute.setStripPrefix(true);
        zuulRoute.setSensitiveHeaders(new HashSet<>());

        routes.put(zuulRoute.getPath(), zuulRoute);

        return routes;
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76

service/RefreshRouteService.java & 路由刷新服务

/**
 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例
 * 论坛:http://bugstack.cn
 * Create by 付政委 on @2019
 */
@Service
public class RefreshRouteService {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Autowired
    private RouteLocator routeLocator;

    public void refreshRoute() {
        RoutesRefreshedEvent routesRefreshedEvent = new RoutesRefreshedEvent(routeLocator);
        publisher.publishEvent(routesRefreshedEvent);
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

ZuulApplication.java & 启动服务注意注解,另外提供了服务接口

/**
 * 微信公众号:bugstack虫洞栈 | 专注原创技术专题案例
 * 论坛:http://bugstack.cn
 * Create by 付政委 on @2019
 */
@SpringBootApplication
@EnableZuulProxy
@EnableEurekaClient
@EnableDiscoveryClient
@RestController
public class ZuulApplication {

    public static void main(String[] args) {
        SpringApplication.run(ZuulApplication.class, args);
    }

    @Bean
    public TokenFilter tokenFilter() {
        return new TokenFilter();
    }

    @Autowired
    private RefreshRouteService refreshRouteService;
    @Autowired
    private ZuulHandlerMapping zuulHandlerMapping;

    @RequestMapping("api/refresh")
    public String refresh(){
        refreshRouteService.refreshRoute();
        return "success";
    }

    @RequestMapping("api/queryRouteInfo")
    @ResponseBody
    public Map<String, Object> queryRouteInfo(){
        return zuulHandlerMapping.getHandlerMap();
    }

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40

application.yml & 配置文件修改,路由过滤

server:
  port: 10001

spring:
  application:
    name: itstack-demo-ddd-zuul

eureka:
  client:
    serviceUrl:
      defaultZone: http://localhost:7397/eureka/

# 动态路由,以下配置注释;
# http://localhost:10001/route-a/api/queryUserInfo?userId=111
# http://localhost:10001/route-b/api/queryUserInfo?userId=111
zuul:
   ignoredPatterns: /api/**
#  routes:
#    api-a:
#      path: /route-a/**
#      serviceId: itstack-demo-springcloud-feign
#    api-b:
#      path: /route-b/**
#      serviceId: itstack-demo-springcloud-ribbon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# 测试验证

  1. 分别启动如下服务;
    1. itstack-demo-springcloud-eureka-server 服务注册与发现
    2. itstack-demo-springcloud-eureka-client 接口提供方
    3. itstack-demo-springcloud-hystrix-feign 调用端
    4. itstack-demo-springcloud-hystrix-ribbon 调用端
    5. itstack-demo-springcloud-zuul 路由服务
  2. 可测试接口列表;
    1. 路由服务:http://localhost:10001/route-a/api/queryUserInfo?userId=111&token=111
      Hi 微信公众号:bugstack虫洞栈 | 111 >: from eureka client port: 8001 From Feign
      
      1
    2. 刷新配置:http://localhost:10001/api/refresh
    3. 内容配置:http://localhost:10001/api/queryRouteInfo

# 综上总结

  1. 路由服务可以方便的帮我们控制业务类型的区分访问,同时自动刷新可以更加方便的使用网关路由
  2. 权限验证是几乎不可少的在实际开发过程中会经常用到,所有的接口必须是安全可靠的,保证数据不泄露
  3. 另外还可以考虑从入参的用户身份进行路由,这样可以把数据库路由提前,让不同用户组直接访问到不同的数据库组

# 文章汇总

  1. Spring Cloud(零)《总有一偏概述告诉你SpringCloud是什么》 (opens new window)
  2. Spring Cloud(一)《服务集群注册与发现 Eureka》 (opens new window)
  3. Spring Cloud(二)《服务提供与负载均衡调用 Eureka》 (opens new window)
  4. Spring Cloud(三)《应用服务快速失败熔断降级保护 Hystrix》 (opens new window)
  5. Spring Cloud(四)《服务响应性能成功率监控 Hystrix》 (opens new window)
  6. Spring Cloud(五)《Turbine 监控信息聚合展示 Hystrix》 (opens new window)
  7. Spring Cloud(六)《基于github webhook动态刷新服务配置》 (opens new window)
  8. Spring Cloud(七)《基于RabbitMQ消息总线方式刷新配置服务》 (opens new window)
  9. Spring Cloud(八)《服务网关路由 Zuul1》 (opens new window)

微信搜索「bugstack虫洞栈」公众号,关注后回复「SpringCloud专题」获取本文源码&更多原创专题案例!